package tsdb

import (
	"fmt"
	"log"
	"time"
)

// AggFunc is the type of a function for aggregating a timeseries.
type AggFunc func([]SeriesResponse) []SeriesResponse

type AggSpec struct {
	Names []string
	Span  time.Duration
}

const (
	EOL dbError = iota
	ErrInvalidValue
)

type dbError int

func (e dbError) Error() string {
	switch e {
	case EOL:
		return "EOL"
	case ErrInvalidValue:
		return "ErrInvalidValue"
	}
	return string(e)
}

// TsDb is the interface any timeseries database backend must satisfy.
type TsDb interface {
	// GetMultipleSeries returns a channel of SeriesResponses for a given
	// list of metrics. Each value in a Datapoint slice corresponds to the
	// corresponding metric at that index.
	//
	// The implementors are responsible for ensuring that
	// for r := range channel r.Val.Value[i] is the result for
	// metrics[i] for each response sent on the channel.
	// TODO(jwall): Return an actual error.
	GetMultipleSeries(metrics []Metric, end time.Time, d time.Duration) []SeriesResponse
	// Add adds a value at the given time.Time for a particular Metric.
	Add(metric Metric, value float64, ts time.Time) error
	// Ticker returns a channel that returns the current value for a timeseries every interval
	Ticker(metrics []Metric, d time.Duration) (chan<- bool, <-chan *SeriesResponse)
}

// Aggregator wraps a TsDb and supplies some aggregating methods
// for any timeseries.
type Aggregator struct {
	fs map[string]IndexedRollFunc
	TsDb
}

func (a *Aggregator) maybeInitMap() {
	if a.fs == nil {
		a.fs = make(map[string]IndexedRollFunc)
	}
}

// RegisterCommon registers some common aggregation functions with an
// Aggregator.
func (a *Aggregator) RegisterCommon() error {
	err := a.RegisterAggregator("Avg", RollAvg)
	if err != nil {
		return err
	}
	err = a.RegisterAggregator("Rate", RollRate)
	if err != nil {
		return err
	}
	err = a.RegisterAggregator("Identity", IdentityRollFunc)
	if err != nil {
		return err
	}
	return err
}

// RegisterAggregator registers an AggFunc under a given name for an Aggregator.
// It returns an error if the name is already taken.
func (a *Aggregator) RegisterAggregator(name string, f IndexedRollFunc) error {
	if _, ok := a.fs[name]; ok {
		return fmt.Errorf(
			"There is already an IndexedRullFunc registered for %q", name)
	}
	a.maybeInitMap()
	a.fs[name] = f
	return nil
}

// MustRegisterAggregator registers an AggFunc under a given name for an
// Aggregator. It panics if the name is already taken.
func (a *Aggregator) MustRegisterAggregator(name string, f IndexedRollFunc) {
	if err := a.RegisterAggregator(name, f); err != nil {
		panic(err)
	}
}

func (a *Aggregator) aggIndexMap(aggs AggSpec) (map[string][]int, error) {
	fs := make(map[string][]int)
	for i, name := range aggs.Names {
		_, ok := a.fs[name]
		if !ok && name != "" {
			return nil, fmt.Errorf("No such aggregation function %q", name)
		} else {
			fs[name] = append(fs[name], i)
		}
	}
	return fs, nil
}

// GetMultipleAggregations returns a a set of aggregations over metrics.
func (a *Aggregator) GetMultipleAggregations(
	metrics []Metric, aggs AggSpec, end time.Time, d time.Duration) []SeriesResponse {
	fs, err := a.aggIndexMap(aggs)
	if err != nil {
		return []SeriesResponse{SeriesResponse{E: err}}
	}
	log.Printf("Calling aggregations with %#v", aggs)
	return a.aggregate(metrics, IndexedRollFuncMuxer(a.fs, fs, aggs.Span), end, d)
}

func (a *Aggregator) Add(metric Metric, value float64, ts time.Time, as ...AggSpec) error {
	// add original value.
	a.TsDb.Add(metric, value, ts)
	// now if we have aggregations do those as well.
	for _, aggs := range as {
		fs, err := a.aggIndexMap(aggs)
		if err != nil {
			return err
		}
		data := a.aggregate(
			[]Metric{metric}, IndexedRollFuncMuxer(a.fs, fs, aggs.Span),
			ts.Add(-aggs.Span), aggs.Span)
		var sr SeriesResponse
		for _, sr = range data {
		} // Get the last item.
		if len(sr.Val.Value) != 0 { // actual data gotten back
			for name, idxs := range fs {
				for _, idx := range idxs {
					if val := sr.Val.Value[idx]; val != nil {
						metric.Var += name
						a.TsDb.Add(metric, *val, sr.Val.Ts)
					}
				}
			}
		}
	}
	return nil
}

func (a *Aggregator) aggregate(
	metrics []Metric, agg AggFunc, end time.Time, d time.Duration) []SeriesResponse {
	// TODO(jwall) Now mux these together :-)
	data := a.TsDb.GetMultipleSeries(metrics, end, d)
	//log.Printf("XXX: Got data from datastore: ", data)
	return agg(data)
}

// Rolldata is the struct that RollFuncs use to calculate their
// rolling aggregations.
type RollData struct {
	S    time.Time // Start of interval
	L    time.Time // End of interval
	SV   []float64 // Values at start of interval
	AccV []float64 // Accumulation of the Values
	LV   []float64 // Values at end of interval
	Min  []float64 // Min values
	Max  []float64 // Max values
	Cnt  int       // Number of entries in the interval
}

func remove(s []*RollData, i int) int {
	ln := len(s)
	if i >= ln {
		return ln
	} else {
		copy(s[i:], s[i+1:])
	}
	return ln - 1
}
